Repository navigation
Stop a thread spawning workers in another project's repo (#109) - #111
Merged
currentbits merged 1 commit intoAug 16, 2026
Merged
Conversation
An agent working in Shipnest-v2 called thread_fork with a thread id it picked off threads_list by title. The id belonged to Solenta, so two grok workers spawned on git worktrees of the wrong repo. Told off, it repeated the mistake four minutes later with a second guessed id. Its own diagnosis was correct: threads_list returns id, title, provider, status and handoffFrom, and nothing about the workspace, so every fork was a guess. The orchestrator has no caller identity either — one workspace-wide bearer token over stateless HTTP — so the server could not tell. Three changes: - threads_list reports projectId and projectName per thread. - Every dispatched prompt carries a [Thread] note stating the run's own thread id, project and cwd, so the ids can be passed instead of guessed. Gated on coder-threads being registered, like planboardNoteFor's GitHub-origin gate. - thread_fork and thread_send require a stated projectId and reject a thread outside it, naming both projects. A same-project fork of a sibling thread still works; only the boundary is blocked. The assert is a self-declared claim, not proof: it catches a confused caller, not a malicious one. Real caller identity needs per-run bearer tokens, which grok and kimi cannot receive — they register MCP globally at user scope. Marked with a ponytail: comment naming that ceiling. Issue #109 asked for cross-project reach. What existed was accidental cross-project reach with no guard rail, so this closes the hole instead; cross-project writes stay off the table and read-only reach is deferred. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
currentbits
deleted the
coder/cross-project-work-a-thread-in-one-proje-d587c3
branch
October 3, 2026 16:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the hole behind #109. The issue asked for cross-project reach; investigation found the workspace already had accidental cross-project reach, with no guard rail. Willem's ruling was that a run should never write to a second repo, so this closes the hole rather than building the feature.
What happened
Shipnest-v2 thread, 2026-08-16 12:48–13:10. An agent working in
/Users/willem/code/Shipnest-v2calledthread_fork({threadId: "260be2c1"})— a thread belonging to Solenta. Two grok workers spawned on git worktrees of the wrong repo. Told off, it repeated the mistake four minutes later with9fafc689, guessed from the title "changelog and website". Cleanup cost two deleted branches and a removed worktree.Its own diagnosis, from the transcript, was correct:
Root cause
thread_fork(threadId)takes its source thread as an argument, andelectron/orchServer.jshas no caller identity at all — one workspace-wide bearer token over stateless HTTP. So the server cannot know who is calling, and the caller must name a thread.Naming one was necessarily a guess, because the agent is never told its own thread id, and
threads_listnever reported which project a thread belonged to.thread_sendhad the same hole: it started a run on any threadId with no check at all. Guarded here too — one guard, both callers.Changes
threads_listreports the project. Each row gainsprojectIdandprojectName— the field whose absence the agent named as the reason it guessed.services.selfIdNoteFor, a sibling of the existingplanboardNoteFor, appends a CLI-only[Thread]note (never stored in the transcript) at the single prompt finalization point inrunner.js, stating the run's own thread id, project and cwd. It rides every dispatch rather than the first turn only, so context compaction and resumed sessions don't lose it. Gated oncoder-threadsactually being registered — with no thread tools in the run there is nothing to pass the ids to — mirroringplanboardNoteFor's GitHub-origin gate.thread_forkandthread_sendrequire a statedprojectIdand reject a thread outside it, naming both projects in the error. A same-project fork of a sibling thread still works; only the project boundary is blocked.Because each thread stays bound to exactly one project, the diff/commit/PR/worktree views are untouched — which answers all three of #109's original open questions with "nothing changes".
thread_statusis deliberately left unguarded: it is read-only, cannot start anything, andthreads_listalready exposes every thread's title across projects.Known ceiling
The assert is a self-declared claim, not proof — it catches a confused caller, not a malicious one. Real caller identity would need per-run bearer tokens mapped to thread ids. Rejected as too expensive for this failure mode: claude and codex take per-run MCP config, but grok and kimi register the server globally (
grok mcp add … -s user,~/.kimi-code/mcp.json), so a per-run token cannot reach them without racy global config rewrites — and the incident's workers were grok. Marked with aponytail:comment naming the upgrade path.Testing
1597 tests green across all four suites: core 38, renderer 686, electron 754, memory 120. Zero failures.
New coverage in
electron/test/orch-server.test.js, which drives the handlers spawn-free via the exportedcreateToolHandlers:selfIdNoteForstates the ids, and returns""whilecoder-threadsis downDeferred
Read-only reach into another project ("go look at how X does this") is left unbuilt until this fix has been lived with. Cross-project writes stay off the table.
Design doc:
docs/superpowers/specs/2026-08-16-cross-project-spawn-guard-design.md